/* Copyright 2009
 *
 * This program and the accompanying materials
 * are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at
 *
 * 		http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * Contributors:
 * 	   IBM Corporation - initial API and implementation for JDT/DLTK
 *     Sean W. Quinn - initial adoption for use with PHP from various sources.
 */
package org.eclipse.php.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.internal.core.ModelManager;
import org.eclipse.dltk.internal.core.util.Util;
import org.eclipse.php.core.compiler.IScanner;
import org.eclipse.php.core.formatter.PHPCodeFormatter;
import org.eclipse.php.core.formatter.PHPCodeFormatterConstants;
import org.eclipse.php.core.util.IScriptFileReader;
import org.eclipse.php.core.util.ScriptFormatException;
import org.eclipse.php.internal.compiler.util.SuffixConstants;
import org.eclipse.php.internal.core.NamespaceFragment;
import org.eclipse.php.internal.core.PHPCorePluginExt;
import org.eclipse.php.internal.core.PHPElement;
import org.eclipse.php.internal.core.PharNamespaceFragmentRoot;
import org.eclipse.php.internal.core.ast.util.UtilExt;
import org.eclipse.php.internal.core.util.ScriptFileReader;

/**
 * Factory for creating various compiler tools, such as scanners, parsers and
 * compilers.
 * <p>
 * This class provides static methods only.
 * </p>
 *
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
@SuppressWarnings("restriction")
public class ToolFactory {

	/**
	 * This mode is used for formatting new code when some formatter options
	 * should not be used. In particular, options that preserve the indentation
	 * of comments are not used. In the future, newly added options may be
	 * ignored as well.
	 * <p>
	 * Clients that are formatting new code are recommended to use this mode.
	 * </p>
	 *
	 * @see PHPCodeFormatterConstants#FORMATTER_NEVER_INDENT_BLOCK_COMMENTS_ON_FIRST_COLUMN
	 * @see PHPCodeFormatterConstants#FORMATTER_NEVER_INDENT_LINE_COMMENTS_ON_FIRST_COLUMN
	 * @see #createPHPCodeFormatter(Map, int)
	 * @since 3.3
	 */
	public static final int M_FORMAT_NEW = new Integer(0).intValue();

	/**
	 * This mode is used for formatting existing code when all formatter options
	 * should be used. In particular, options that preserve the indentation of
	 * comments are used.
	 * <p>
	 * Clients that are formatting existing code are recommended to use this
	 * mode.
	 * </p>
	 *
	 * @see PHPCodeFormatterConstants#FORMATTER_NEVER_INDENT_BLOCK_COMMENTS_ON_FIRST_COLUMN
	 * @see PHPCodeFormatterConstants#FORMATTER_NEVER_INDENT_LINE_COMMENTS_ON_FIRST_COLUMN
	 * @see #createPHPCodeFormatter(Map, int)
	 * @since 3.3
	 */
	public static final int M_FORMAT_EXISTING = new Integer(1).intValue();

	/**
	 * Create an instance of the built-in code formatter.
	 * <p>
	 * The given options should at least provide the source level (
	 * {@link PHPCorePluginExt#COMPILER_SOURCE}), the compiler compliance level
	 * ({@link PHPCorePluginExt#COMPILER_COMPLIANCE}) and the target platform (
	 * {@link PHPCorePluginExt#COMPILER_CODEGEN_TARGET_PLATFORM}). Without these
	 * options, it is not possible for the code formatter to know what kind of
	 * source it needs to format.
	 * </p>
	 * <p>
	 * Note this is equivalent to
	 * <code>createPHPCodeFormatter(options, M_FORMAT_NEW)</code>. Thus some
	 * code formatter options may be ignored. See @{link {@link #M_FORMAT_NEW}
	 * for more details.
	 * </p>
	 *
	 * @param options - the options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>PHPCorePluginExt#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>PHPCorePluginExt#getOptions</code>.
	 * @return an instance of the built-in code formatter
	 * @see PHPCodeFormatter
	 * @see PHPCorePluginExt#getOptions()
	 * @since 3.0
	 */
	public static PHPCodeFormatter createPHPCodeFormatter(Map options) {
		return createPHPCodeFormatter(options, M_FORMAT_NEW);
	}

	/**
	 * Create an instance of the built-in code formatter.
	 * <p>
	 * The given options should at least provide the source level (
	 * {@link PHPCorePluginExt#COMPILER_SOURCE}), the compiler compliance level
	 * ({@link PHPCorePluginExt#COMPILER_COMPLIANCE}) and the target platform (
	 * {@link PHPCorePluginExt#COMPILER_CODEGEN_TARGET_PLATFORM}). Without these
	 * options, it is not possible for the code formatter to know what kind of
	 * source it needs to format.
	 * </p>
	 * <p>
	 * The given mode determines what options should be enabled when formatting
	 * the code. It can have the following values: {@link #M_FORMAT_NEW},
	 * {@link #M_FORMAT_EXISTING}, but other values may be added in the future.
	 * </p>
	 *
	 * @param options the options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>PHPCorePluginExt#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>PHPCorePluginExt#getOptions</code>.
	 * @param mode the given mode to modify the given options.
	 * @return an instance of the built-in code formatter
	 * @see PHPCodeFormatter
	 * @see PHPCorePluginExt#getOptions()
	 * @since 3.3
	 */
	public static PHPCodeFormatter createPHPCodeFormatter(Map options, int mode) {
		//if (options == null) {
		//	options = PHPCorePluginExt.getOptions();
		//}
		//Map currentOptions = new HashMap(options);
		//if (mode == M_FORMAT_NEW) {
		// disable the option for not indenting comments starting on first column
		//	currentOptions.put(PHPCodeFormatterConstants.FORMATTER_NEVER_INDENT_BLOCK_COMMENTS_ON_FIRST_COLUMN,
		//			PHPCodeFormatterConstants.FALSE);
		//	currentOptions.put(PHPCodeFormatterConstants.FORMATTER_NEVER_INDENT_LINE_COMMENTS_ON_FIRST_COLUMN,
		//			PHPCodeFormatterConstants.FALSE);
		//}
		//return new PHPCodeFormatter(currentOptions);
		// FIXME: Implement me!
		throw new RuntimeException(ToolFactory.class.getName() + "#createPHPCodeFormatter() not yet implemented!");
	}

	/**
	 * Create a classfile reader onto a classfile Java element. Create a default
	 * classfile reader, able to expose the internal representation of a given
	 * classfile according to the decoding flag used to initialize the reader.
	 * Answer null if the file named fileName doesn't represent a valid .class
	 * file. The decoding flags are described in IScriptFileReader.
	 *
	 * @param classfile the classfile element to introspect
	 * @param decodingFlag the flag used to decode the class file reader.
	 * @return a default classfile reader
	 * @see IScriptFileReader
	 */
	public static IScriptFileReader createDefaultScriptFileReader(IScriptFile scriptFile, int decodingFlag) {

		INamespaceFragmentRoot root = (INamespaceFragmentRoot) scriptFile
				.getAncestor(IPHPElement.NAMESPACE_FRAGMENT_ROOT);
		if (root != null) {
			try {
				if (root instanceof PharNamespaceFragmentRoot) {
					String archiveName = null;
					ZipFile Phar = null;
					try {
						Phar = ((PharNamespaceFragmentRoot) root).getPhar();
						archiveName = Phar.getName();
					}
					finally {
						ModelManager.getModelManager().closeZipFile(Phar);
					}
					NamespaceFragment NamespaceFragment = (NamespaceFragment) scriptFile.getParent();
					String scriptFileName = scriptFile.getElementName();
					String entryName = Util.concatWith(NamespaceFragment.names, scriptFileName, '/');
					return createDefaultScriptFileReader(archiveName, entryName, decodingFlag);
				}
				else {
					InputStream in = null;
					try {
						in = ((IFile) ((PHPElement) scriptFile).resource()).getContents();
						return createDefaultScriptFileReader(in, decodingFlag);
					}
					finally {
						if (in != null)
							try {
								in.close();
							}
							catch (IOException e) {
								// ignore
							}
					}
				}
			}
			catch (CoreException e) {
				// unable to read
			}
		}
		return null;
	}

	/**
	 * Create a default classfile reader, able to expose the internal
	 * representation of a given classfile according to the decoding flag used
	 * to initialize the reader. Answer null if the input stream contents cannot
	 * be retrieved The decoding flags are described in IScriptFileReader.
	 *
	 * @param stream the given input stream to read
	 * @param decodingFlag the flag used to decode the class file reader.
	 * @return a default classfile reader
	 * @see IScriptFileReader
	 * @since 3.2
	 */
	public static IScriptFileReader createDefaultScriptFileReader(InputStream stream, int decodingFlag) {
		try {
			return new ScriptFileReader(UtilExt.getInputStreamAsByteArray(stream, -1), decodingFlag);
		}
		catch (ScriptFormatException e) {
			return null;
		}
		catch (IOException e) {
			return null;
		}
	}

	/**
	 * Create a default classfile reader, able to expose the internal
	 * representation of a given classfile according to the decoding flag used
	 * to initialize the reader. Answer null if the file named fileName doesn't
	 * represent a valid .class file. The fileName has to be an absolute OS path
	 * to the given .class file. The decoding flags are described in
	 * IScriptFileReader.
	 *
	 * @param fileName the name of the file to be read
	 * @param decodingFlag the flag used to decode the class file reader.
	 * @return a default classfile reader
	 * @see IScriptFileReader
	 */
	public static IScriptFileReader createDefaultScriptFileReader(String fileName, int decodingFlag) {
		try {
			return new ScriptFileReader(UtilExt.getFileByteContent(new File(fileName)), decodingFlag);
		}
		catch (ScriptFormatException e) {
			return null;
		}
		catch (IOException e) {
			return null;
		}
	}

	/**
	 * Create a default classfile reader, able to expose the internal
	 * representation of a given classfile according to the decoding flag used
	 * to initialize the reader. Answer null if the file named zipFileName
	 * doesn't represent a valid zip file or if the zipEntryName is not a valid
	 * entry name for the specified zip file or if the bytes don't represent a
	 * valid .class file according to the JVM specifications. The decoding flags
	 * are described in IScriptFileReader.
	 *
	 * @param zipFileName the name of the zip file
	 * @param zipEntryName the name of the entry in the zip file to be read
	 * @param decodingFlag the flag used to decode the class file reader.
	 * @return a default classfile reader
	 * @see IScriptFileReader
	 */
	public static IScriptFileReader createDefaultScriptFileReader(String zipFileName, String zipEntryName,
			int decodingFlag) {
		ZipFile zipFile = null;
		try {
			if (ModelManager.ZIP_ACCESS_VERBOSE) {
				System.out
						.println("(" + Thread.currentThread() + ") [ToolFactory.createDefaultScriptFileReader()] Creating ZipFile on " + zipFileName); //$NON-NLS-1$	//$NON-NLS-2$
			}
			zipFile = new ZipFile(zipFileName);
			ZipEntry zipEntry = zipFile.getEntry(zipEntryName);
			if (zipEntry == null) {
				return null;
			}
			if (!zipEntryName.toLowerCase().endsWith(SuffixConstants.SUFFIX_STRING_php)) {
				return null;
			}
			byte classFileBytes[] = UtilExt.getZipEntryByteContent(zipEntry, zipFile);
			return new ScriptFileReader(classFileBytes, decodingFlag);
		}
		catch (ScriptFormatException e) {
			return null;
		}
		catch (IOException e) {
			return null;
		}
		finally {
			if (zipFile != null) {
				try {
					zipFile.close();
				}
				catch (IOException e) {
					// ignore
				}
			}
		}
	}

	/**
	 * Create a scanner, indicating the level of detail requested for
	 * tokenizing. The scanner can then be used to tokenize some source in a
	 * Java aware way. Here is a typical scanning loop: <code>
	 * <pre>
	 *   IScanner scanner = ToolFactory.createScanner(false, false, false, false);
	 *   scanner.setSource("int i = 0;".toCharArray());
	 *   while (true) {
	 *     int token = scanner.getNextToken();
	 *     if (token == ITerminalSymbols.TokenNameEOF) break;
	 *     System.out.println(token + " : " + new String(scanner.getCurrentTokenSource()));
	 *   }
	 * </pre>
	 * </code>
	 * <p>
	 * The returned scanner will tolerate unterminated line comments (missing
	 * line separator). It can be made stricter by using API with extra boolean
	 * parameter (<code>strictCommentMode</code>).
	 * <p>
	 *
	 * @param tokenizeComments if set to <code>false</code>, comments will be
	 *            silently consumed
	 * @param tokenizeWhiteSpace if set to <code>false</code>, white spaces will
	 *            be silently consumed,
	 * @param assertMode if set to <code>false</code>, occurrences of 'assert'
	 *            will be reported as identifiers (
	 *            <code>ITerminalSymbols#TokenNameIdentifier</code>), whereas if
	 *            set to <code>true</code>, it would report assert keywords (
	 *            <code>ITerminalSymbols#TokenNameassert</code>). Java 1.4 has
	 *            introduced a new 'assert' keyword.
	 * @param recordLineSeparator if set to <code>true</code>, the scanner will
	 *            record positions of encountered line separator ends. In case
	 *            of multi-character line separators, the last character
	 *            position is considered. These positions can then be extracted
	 *            using <code>IScanner#getLineEnds</code>. Only non-unicode
	 *            escape sequences are considered as valid line separators.
	 * @return a scanner
	 * @see org.eclipse.jdt.core.compiler.IScanner
	 */
	public static IScanner createScanner(boolean tokenizeComments, boolean tokenizeWhiteSpace, boolean assertMode,
			boolean recordLineSeparator) {
		// FIXME: Implement me!
		throw new RuntimeException(ToolFactory.class.getName() + "#createScanner() not yet implemented!");
	}

	/**
	 * Create a scanner, indicating the level of detail requested for
	 * tokenizing. The scanner can then be used to tokenize some source in a
	 * Java aware way. Here is a typical scanning loop: <code>
	 * <pre>
	 *   IScanner scanner = ToolFactory.createScanner(false, false, false, false);
	 *   scanner.setSource("int i = 0;".toCharArray());
	 *   while (true) {
	 *     int token = scanner.getNextToken();
	 *     if (token == ITerminalSymbols.TokenNameEOF) break;
	 *     System.out.println(token + " : " + new String(scanner.getCurrentTokenSource()));
	 *   }
	 * </pre>
	 * </code>
	 * <p>
	 * The returned scanner will tolerate unterminated line comments (missing
	 * line separator). It can be made stricter by using API with extra boolean
	 * parameter (<code>strictCommentMode</code>).
	 * <p>
	 *
	 * @param tokenizeComments if set to <code>false</code>, comments will be
	 *            silently consumed
	 * @param tokenizeWhiteSpace if set to <code>false</code>, white spaces will
	 *            be silently consumed,
	 * @param recordLineSeparator if set to <code>true</code>, the scanner will
	 *            record positions of encountered line separator ends. In case
	 *            of multi-character line separators, the last character
	 *            position is considered. These positions can then be extracted
	 *            using <code>IScanner#getLineEnds</code>. Only non-unicode
	 *            escape sequences are considered as valid line separators.
	 * @param sourceLevel if set to <code>&quot;1.3&quot;</code> or
	 *            <code>null</code>, occurrences of 'assert' will be reported as
	 *            identifiers (<code>ITerminalSymbols#TokenNameIdentifier</code>
	 *            ), whereas if set to <code>&quot;1.4&quot;</code>, it would
	 *            report assert keywords (
	 *            <code>ITerminalSymbols#TokenNameassert</code>). Java 1.4 has
	 *            introduced a new 'assert' keyword.
	 * @return a scanner
	 * @see org.eclipse.jdt.core.compiler.IScanner
	 * @since 3.0
	 */
	public static IScanner createScanner(boolean tokenizeComments, boolean tokenizeWhiteSpace,
			boolean recordLineSeparator, String sourceLevel) {
		// FIXME: Implement me!
		throw new RuntimeException(ToolFactory.class.getName() + "#createScanner() not yet implemented!");
	}

	/**
	 * Create a scanner, indicating the level of detail requested for
	 * tokenizing. The scanner can then be used to tokenize some source in a
	 * Java aware way. Here is a typical scanning loop: <code>
	 * <pre>
	 *   IScanner scanner = ToolFactory.createScanner(false, false, false, false);
	 *   scanner.setSource("int i = 0;".toCharArray());
	 *   while (true) {
	 *     int token = scanner.getNextToken();
	 *     if (token == ITerminalSymbols.TokenNameEOF) break;
	 *     System.out.println(token + " : " + new String(scanner.getCurrentTokenSource()));
	 *   }
	 * </pre>
	 * </code>
	 * <p>
	 * The returned scanner will tolerate unterminated line comments (missing
	 * line separator). It can be made stricter by using API with extra boolean
	 * parameter (<code>strictCommentMode</code>).
	 * <p>
	 *
	 * @param tokenizeComments if set to <code>false</code>, comments will be
	 *            silently consumed
	 * @param tokenizeWhiteSpace if set to <code>false</code>, white spaces will
	 *            be silently consumed,
	 * @param recordLineSeparator if set to <code>true</code>, the scanner will
	 *            record positions of encountered line separator ends. In case
	 *            of multi-character line separators, the last character
	 *            position is considered. These positions can then be extracted
	 *            using <code>IScanner#getLineEnds</code>. Only non-unicode
	 *            escape sequences are considered as valid line separators.
	 * @param sourceLevel if set to <code>&quot;1.3&quot;</code> or
	 *            <code>null</code>, occurrences of 'assert' will be reported as
	 *            identifiers (<code>ITerminalSymbols#TokenNameIdentifier</code>
	 *            ), whereas if set to <code>&quot;1.4&quot;</code>, it would
	 *            report assert keywords (
	 *            <code>ITerminalSymbols#TokenNameassert</code>). Java 1.4 has
	 *            introduced a new 'assert' keyword.
	 * @param complianceLevel This is used to support the Unicode 4.0 character
	 *            sets. if set to 1.5 or above, the Unicode 4.0 is supporte,
	 *            otherwise Unicode 3.0 is supported.
	 * @return a scanner
	 * @see org.eclipse.jdt.core.compiler.IScanner
	 * @since 3.1
	 */
	public static IScanner createScanner(boolean tokenizeComments, boolean tokenizeWhiteSpace,
			boolean recordLineSeparator, String sourceLevel, String complianceLevel) {
		// FIXME: Implement me!
		throw new RuntimeException(ToolFactory.class.getName() + "#createScanner() not yet implemented!");
	}
}
